/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.huawei.healthecology.processor;

import com.huawei.healthecology.data.ble.callback.BleCharacteristicValueChangeCallback;
import com.huawei.healthecology.data.ble.callback.BleConnectionChangeCallback;
import com.huawei.healthecology.data.ble.callback.BleMtuUpdatedCallback;
import com.huawei.healthecology.data.ble.callback.BleServiceDiscoveredCallback;
import com.huawei.healthecology.data.ble.callback.EnableNotifyIndicateCallback;
import com.huawei.healthecology.data.ble.callback.ReadBleCharacteristicValueCallback;
import com.huawei.healthecology.data.ble.callback.ReadBleDescriptorValueCallback;
import com.huawei.healthecology.data.ble.callback.WriteBleCharacteristicValueCallback;
import com.huawei.healthecology.data.ble.callback.WriteBleDescriptorValueCallback;
import com.huawei.healthecology.data.ble.data.BleConnectionState;
import com.huawei.healthecology.data.ble.data.BleServiceDiscoveryResult;
import com.huawei.healthecology.data.ble.data.CharacteristicChangeData;
import com.huawei.healthecology.data.ble.data.MtuUpdateData;
import com.huawei.healthecology.data.ble.response.DescriptorReadResponse;
import com.huawei.healthecology.data.utils.BluetoothProfileByteUtil;
import com.huawei.healthecology.data.utils.CallbackProvider;
import com.huawei.healthecology.data.utils.ConditionOperation;
import com.huawei.healthecology.data.utils.OptionalX;
import com.huawei.healthecology.log.LogUtil;

import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import ohos.bluetooth.ble.BlePeripheralCallback;
import ohos.bluetooth.ble.BlePeripheralDevice;
import ohos.bluetooth.ble.GattCharacteristic;
import ohos.bluetooth.ble.GattDescriptor;

import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;

/**
 * Gatt event monitor
 */
@Data
@RequiredArgsConstructor(staticName = "get")
public class GattEventMonitor {
    private static final String TAG = "GattEventMonitor";

    private CallbackProvider<BleMtuUpdatedCallback> mtuUpdatedProvider;

    private CallbackProvider<BleConnectionChangeCallback> connectionChangeProvider;

    private CallbackProvider<BleServiceDiscoveredCallback> serviceDiscoveredProvider;

    private CallbackProvider<ReadBleCharacteristicValueCallback> readCharacteristicProvider;

    private CallbackProvider<WriteBleCharacteristicValueCallback> writeCharacteristicProvider;

    private CallbackProvider<BleCharacteristicValueChangeCallback> characteristicValueChangeProvider;

    private CallbackProvider<ReadBleDescriptorValueCallback> readDescriptorProvider;

    private CallbackProvider<WriteBleDescriptorValueCallback> writeDescriptorProvider;

    private CallbackProvider<EnableNotifyIndicateCallback> enableNotifyIndicationProvider;

    /**
     * Get Ble peripheral callback
     *
     * @param peripheralDevice Ble peripheral device
     * @return BlePeripheralCallback
     */
    public BlePeripheralCallback getBlePeripheralCallback(@NonNull BlePeripheralDevice peripheralDevice) {
        return new BlePeripheralCallback() {
            @Override
            public void connectionStateChangeEvent(int connectionState) {
                onConnectionStateChangeEvent(peripheralDevice, connectionState);
            }

            @Override
            public void servicesDiscoveredEvent(int status) {
                if (status == BlePeripheralDevice.OPERATION_SUCC) {
                    BleServiceDiscoveredCallback discoverCallback = Optional.ofNullable(serviceDiscoveredProvider)
                        .flatMap(CallbackProvider::getLast)
                        .orElse(null);
                    String deviceId = Optional.ofNullable(peripheralDevice)
                        .map(BlePeripheralDevice::getDeviceAddr)
                        .orElse(null);
                    OptionalX.ofNullable(discoverCallback)
                        .ifPresent(callback -> callback.onBleServiceDiscovered(
                            BleServiceDiscoveryResult.of(deviceId, true)))
                        .ifNotPresent(() -> LogUtil.debug(TAG, "ConnectionStateChangeCallback is empty"));
                }
            }

            @Override
            public void characteristicReadEvent(GattCharacteristic characteristic, int ret) {
                onCharacteristicReadEvent(peripheralDevice, characteristic, ret);
            }

            @Override
            public void characteristicWriteEvent(GattCharacteristic characteristic, int ret) {
                onCharacteristicWriteEvent(peripheralDevice, characteristic, ret);
            }

            @Override
            public void characteristicChangedEvent(GattCharacteristic characteristic) {
                onCharacteristicChangedEvent(peripheralDevice, characteristic);
            }

            @Override
            public void descriptorReadEvent(GattDescriptor descriptor, int ret) {
                ReadBleDescriptorValueCallback valueCallback = Optional.ofNullable(readDescriptorProvider)
                    .flatMap(CallbackProvider::getLast)
                    .orElse(null);
                OptionalX.ofNullable(valueCallback)
                    .ifPresent(callback -> callback.onDescriptorRead(DescriptorReadResponse.builder()
                        .deviceId(peripheralDevice.getDeviceAddr())
                        .descriptorData(BluetoothProfileByteUtil.bytesToHexString(descriptor.getValue())).build()))
                    .ifNotPresent(() -> LogUtil.debug(TAG, "descriptorReadEventCallback is empty"));
            }

            @Override
            public void descriptorWriteEvent(GattDescriptor descriptor, int ret) {
                onDescriptorWriteEvent(peripheralDevice, ret);
            }

            @Override
            public void mtuUpdateEvent(int mtu, int ret) {
                onMtuUpdate(peripheralDevice, mtu, ret);
            }
        };
    }

    /**
     * clear all callback
     */
    public void clearAllCallback() {
        Optional.ofNullable(mtuUpdatedProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(readDescriptorProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(writeDescriptorProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(connectionChangeProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(serviceDiscoveredProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(readCharacteristicProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(writeCharacteristicProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(enableNotifyIndicationProvider).ifPresent(CallbackProvider::clear);
        Optional.ofNullable(characteristicValueChangeProvider).ifPresent(CallbackProvider::clear);
    }

    private void onConnectionStateChangeEvent(@NonNull BlePeripheralDevice blePeripheralDevice, int connectionState) {
        boolean isConnected = (connectionState == BluetoothProfileByteUtil.STATE_CONNECTED);
        LogUtil.info(TAG, "connectionStateChangeEvent isConnected = " + isConnected);
        BleConnectionChangeCallback connectionChangeCallback = Optional.ofNullable(connectionChangeProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        BleServiceDiscoveredCallback discoveredCallback = Optional.ofNullable(serviceDiscoveredProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        callbackDeviceConnection(blePeripheralDevice, connectionChangeCallback, isConnected);
        callbackDeviceServicesDiscovered(blePeripheralDevice, discoveredCallback, isConnected);
    }

    private void onCharacteristicReadEvent(BlePeripheralDevice device, GattCharacteristic characteristic, int ret) {
        LogUtil.info(TAG, "characteristicReadEvent ret = " + ret);
        ReadBleCharacteristicValueCallback readCallback = Optional.ofNullable(readCharacteristicProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        OptionalX.ofNullable(readCallback)
            .ifPresent(callback -> callback.onCharacteristicRead(CharacteristicChangeData.builder()
                .deviceId(device.getDeviceAddr())
                .characteristicId(characteristic.getUuid().toString())
                .serviceId(characteristic.getService().getUuid().toString())
                .characteristicData(BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue())).build()))
            .ifNotPresent(() -> LogUtil.debug(TAG, "ReadCharacteristicCallback is empty"));
    }

    private void onCharacteristicWriteEvent(BlePeripheralDevice device, GattCharacteristic characteristic, int ret) {
        LogUtil.info(TAG, "characteristicReadEvent ret = " + ret);
        WriteBleCharacteristicValueCallback writeCallback = Optional.ofNullable(writeCharacteristicProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        OptionalX.ofNullable(writeCallback)
            .ifPresent(callback -> callback.onCharacteristicWrite(CharacteristicChangeData.builder()
                .deviceId(device.getDeviceAddr())
                .characteristicId(characteristic.getUuid().toString())
                .serviceId(characteristic.getService().getUuid().toString())
                .characteristicData(BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue())).build()))
            .ifNotPresent(() -> LogUtil.debug(TAG, "ReadCharacteristicCallback is empty"));
    }

    private void onCharacteristicChangedEvent(BlePeripheralDevice device, GattCharacteristic characteristic) {
        LogUtil.info(TAG, "characteristicChangedEvent" + Arrays.toString(characteristic.getValue()));
        UUID uuid = characteristic.getUuid();
        String deviceId = Optional.of(device)
            .map(BlePeripheralDevice::getDeviceAddr)
            .orElse(null);
        Long callbackCounts = Optional.ofNullable(characteristicValueChangeProvider)
            .map(callbackProvider -> callbackProvider.getAll().count())
            .orElse(0L);
        ConditionOperation.of(callbackCounts != 0)
            .ifExist(hasCallback -> Optional.ofNullable(uuid)
                .map(UUID::toString)
                .map(id -> CharacteristicChangeData.builder()
                    .characteristicData(BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue()))
                    .characteristicId(id)
                    .deviceId(deviceId).build())
                .ifPresent(data -> Optional.ofNullable(characteristicValueChangeProvider)
                    .map(CallbackProvider::getAll)
                    .ifPresent(allCallback ->
                        allCallback.forEach(callback -> callback.onBleCharacteristicValueChange(data)))));
    }

    private void onDescriptorWriteEvent(BlePeripheralDevice device, int ret) {
        LogUtil.info(TAG, "descriptorWriteEvent ret = " + ret);
        EnableNotifyIndicateCallback notifyIndicateCallback = Optional.ofNullable(enableNotifyIndicationProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        OptionalX.ofNullable(notifyIndicateCallback)
            .ifPresent(callback -> callback.onEnableNotifyIndicate(device.getDeviceAddr()))
            .ifNotPresent(() -> LogUtil.debug(TAG, "enableNotifyOrIndicatorCallback is empty"));
        WriteBleDescriptorValueCallback writeCallback = Optional.ofNullable(writeDescriptorProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        OptionalX.ofNullable(writeCallback)
            .ifPresent(callback -> callback.onDescriptorWrite(device.getDeviceAddr()))
            .ifNotPresent(() -> LogUtil.debug(TAG, "writeDescriptorCallback is empty"));
    }

    private void onMtuUpdate(BlePeripheralDevice device, int mtu, int ret) {
        LogUtil.debug(TAG, "on Mtu Updated result " + ret);
        BleMtuUpdatedCallback updatedCallback = Optional.ofNullable(mtuUpdatedProvider)
            .flatMap(CallbackProvider::getLast)
            .orElse(null);
        OptionalX.ofNullable(updatedCallback)
            .ifPresent(callback -> callback.onBleMtuUpdated(MtuUpdateData.of(device.getDeviceAddr(), mtu)))
            .ifNotPresent(() -> LogUtil.debug(TAG, "ConnectionStateChangeCallback is empty"));
    }

    private void callbackDeviceConnection(@NonNull BlePeripheralDevice blePeripheralDevice,
        BleConnectionChangeCallback changeCallback, boolean isConnected) {
        String deviceName = blePeripheralDevice.getDeviceName().orElse(null);
        OptionalX.ofNullable(changeCallback)
            .ifPresent(callback -> OptionalX.ofNullable(blePeripheralDevice)
                .ifPresent(device -> callback.onBleConnectionStateChange(BleConnectionState.builder()
                    .deviceId(device.getDeviceAddr())
                    .deviceName(deviceName)
                    .isConnected(isConnected).build()))
                .ifNotPresent(() -> LogUtil.debug(TAG, "ConnectionStateChangeCallback with invalid device")))
            .ifNotPresent(() -> LogUtil.debug(TAG, "ConnectionStateChangeCallback is empty"));
    }

    private void callbackDeviceServicesDiscovered(@NonNull BlePeripheralDevice blePeripheralDevice,
        BleServiceDiscoveredCallback discoveredCallback, boolean isConnected) {
        boolean isDiscoverSuccess = Optional.of(blePeripheralDevice)
            .filter(device -> isConnected)
            .map(BlePeripheralDevice::discoverServices)
            .orElse(false);
        String deviceId = Optional.of(blePeripheralDevice)
            .map(BlePeripheralDevice::getDeviceAddr)
            .orElse(null);
        OptionalX.ofNullable(discoveredCallback)
            .ifPresent(callback -> ConditionOperation.of(isDiscoverSuccess)
                .ifNotExist(failed -> callback.onBleServiceDiscovered(BleServiceDiscoveryResult.of(deviceId, false))))
            .ifNotPresent(() -> LogUtil.debug(TAG, "discoveredCallbackCallback is empty"));
    }
}
